/**
 * 定义基础单位基数. 比如10为基数,16基数等. 此处要表示无限大数值,以1000为单位,即逢1000进位.
 */
export const UNIT_BASE = 1000;
// 定义最多显示几位数字.如1.23 显示3位数字.1234 显示4位数字等.
const MAX_LEN = 3;

/**
 * 大数据单位定义, 可根据自己需要进行修改. 大数值显示会自动使用枚举的名称字符串,如100a,300AA, zero 单位不显示.
 * 可无限扩展单位,正常情况下,不需要扩展太多单位.
 * 1b = 1000a
 */
export enum Unit {
    zero = 0,
    // a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,
    A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
    // aa, bb, cc, dd, ee, ff, gg, hh, ii, jj, kk, ll, mm, nn, oo, pp, qq, rr, ss, tt, uu, vv, ww, xx, yy, zz,
    AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR, SS, TT, UU, VV, WW, XX, YY, ZZ
}

// 定义能到达的最大单位, 超过此单位的数据不进位.
const UNIT_MAX = Unit.ZZ;

/**
 * 大数值结构定义,包含大数值的四则运算及数值对比.
 * 原理:
 *      大数值包含若干单位对应的数值,按单位进行hash存储.
 *      如 {1:10,2:10,3:10} => 10c10b10a  表示的实际数值为 10c+10b+10a.
 *      大数值处理的难点在于, 既要保持足够的精度,又能够表示无限大的数值, 同时还要保证较低的频繁运算性能开销.
 * 1. 精度保持: 如果仅靠小数点位数来保证精度,在js中是极不可靠的.
 *          此处采用的方式是利用足够多的单位来分别存储小数位对应的数值. 如 大数值:1c2a,实际表示的数值是 1.000 002 c.
 * 2. 无限大数值: 单纯数字无法表示无限大数据, 因此将以10为基数的传统数字, 拆分为 以1000为基数的大数字,同时单位可以无限扩充,直到满足系统需要,
 *          如:100ZZ 实际能够表示的数值为: 100 x 10^324 数值.
 *
 * 3. 频繁运算性能开销: 本方案将一个无限大数值,拆分为多个单元的小数值进行运算, 中间无需经过复杂的字符串转换,数组分组, 以及小数位取舍等 可能降低精度或增大运算开销的操作.
 *          如: BigNumber(10f 34e 20a 100) X 1000  => 10f x1000 + 34e x1000 + 20a x1000 + 100 x1000 => BigNumber(10000f 34000e 20000a 100000)=>
 *          归一化处理后变为: BigNumber(10g 34f 20b 100a)
 *          连续运算时, 可选择不进行归一化处理,仅在最后处理一次即可.
 *
 * 4. 大数值加法: 两个大数值相加, 直接对相应单位的数值进行合并,因有约定每单位数值在 +-[0-1000)之间,因此相加后不会出现无法表示的数值.
 *          如: B(100c, 50b, 100a) + B(40d,20b, 100) = B(40d,100c,70b, 100a,100)
 *          一般大数值业务中,相加运算极为频繁, 比如每秒产值不停的叠加 计算总产量等. 采用以上计算方式,计算开销只相当于一次map遍历.
 *
 * 5. 大数值减法: 两个大数值减法, 实际是对被减数进行取反后,再进行加法.  因此计算开销实际上为两次 map遍历.
 *          如: B(100c, 50b, 100a) - B(40d,20b, 100) => B(100c, 50b, 100a) + B(-40d,-20b, -100) => B(-40d, 100c, 30b, 100a, -100)
 *
 * 6. 大数值乘法: 两个大数值相乘, 实际上为每一项展开相乘,即 (a+b)x(c+d) = ac + ad +bc +bd. 根据 两数值精度不同, 计算复杂度为: O(m * n), m,n 分别为两个数据中包含的单位数量.
 *          如: B(100c, 50b, 100a) * B(2a, 100) =  2a*B(100c, 50b, 100a) + 100 * B(100c, 50b, 100a)
 *
 * 7. 大数值除法: 两个大数值相除, 无法进行展开计算,因此采取了一种折中方案, 即将除数 转化为 小数进行运算.
 *          如:  B(100c, 50b, 100a) / B(2a, 100) =   B(100c, 50b, 100a) / 2.1a  = B(100c, 50b, 100a) *( 1 / 2.1 a)
 */
export class BigNumber {
    /**
     * 转换大数值字符串为大数值结构.以便进行数值计算.
     * @param value 示例参数: "123.456c"
     */
    public static valueOf(value: string): BigNumber {
        let v2 = parseFloat(value);
        if (v2 == 0 || isNaN(v2) || !value) {
            return new BigNumber();
        }
        const v = value.replace(/[ ]+/g, "");
        const num = new BigNumber();
        const units = v.split(/[0-9.]+/g);
        const digits = v.split(/[a-zA-Z]+/g);
        for (let i = 0; i < digits.length; i++) {
            num.setField(parseFloat(digits[i]) || 0, Unit[units[i + 1]] || Unit.zero);
        }
        return num;
    }

    /**
     * 转换大数值JSON字符串为大数值结构.
     * @param jsonData  示例参数: {1:100,2:-100,c:40}
     */
    public static parse(jsonData: string): BigNumber {
        let num = new BigNumber();
        let data = JSON.parse(jsonData);
        if (typeof (data) !== 'object') {
            return num;
        }
        let unit = Unit.zero;
        for (let u in data) {
            if (!data.hasOwnProperty(u)) {
                continue;
            }
            unit = parseInt(u, 10);
            if (unit >= Unit.zero && unit <= UNIT_MAX) {
                num.data[u] = data[u];
            }
        }
        return num;
    }

    /**
     *  归一化处理.
     *  一个大数值经过多步计算,可能出现每个单位的数值不在+-[0-1000)之间,需要进行进位处理.
     * @param num
     */
    public static normalize(num: BigNumber) {
        let newNumber = new BigNumber();
        // 不确定每个精度单位是否完全满足+-[1,1000)区间,因此需要预处理一波.将每个精度单位归一化为+-[1,1000)的值,并相加.
        for (let u in num.data) {
            if (num.data.hasOwnProperty(u)) {
                newNumber.add(num.data[u], parseInt(u, 10));
            }
        }
        // 处理进位.
        let symb = 1;
        let v2 = 0;
        let u2 = Unit.zero;
        let temp = 0, fireNum = false;
        let unites = newNumber.unites();
        if (unites.length <= 1) {
            return num.copy(newNumber);
        }
        let uMax = unites[unites.length - 1] + 1;
        for (let u = unites[0]; u < uMax; u++) {
            if (u >= UNIT_MAX) {
                break;
            }
            if (newNumber.data.hasOwnProperty(u)) {
                u2 = u;
                v2 = Math.abs(newNumber.data[u]);
                symb = v2 >= 0 ? 1 : -1;
                fireNum = false;
                if (v2 >= UNIT_BASE) {
                    while (v2 >= UNIT_BASE) {
                        temp = v2 - Math.floor(v2 / UNIT_BASE) * UNIT_BASE;
                        if (temp != 0) {
                            if (fireNum) {
                                if (newNumber.data.hasOwnProperty(u2)) {
                                    newNumber.data[u2] += symb * temp;
                                } else {
                                    newNumber.data[u2] = symb * temp;
                                }
                            } else {
                                newNumber.data[u2] = symb * temp;
                                fireNum = true;
                            }
                        }
                        v2 = Math.floor(v2 / UNIT_BASE);
                        u2++;
                        if (u2 >= UNIT_MAX) {
                            break;
                        }
                    }
                    if (newNumber.data.hasOwnProperty(u2)) {
                        newNumber.data[u2] += symb * v2;
                    } else {
                        newNumber.data[u2] = symb * v2;
                    }
                }
            }
        }
        num.copy(newNumber);
    }

    private data: { [key: number]: number; } = {};

    /**
     * 构造函数. 可以生成一个初始的大数值.
     * @param value
     * @param unit
     */
    constructor(value: number = 0, unit: Unit = Unit.zero) {
        if (value == 0) {
            this.data[Unit.zero] = 0;
            return;
        }
        this.formatValue(value, unit);
    }

    /**
     * 转化为字符串存储,用于无损精度的保存完整数值.
     */
    public value(): string {
        BigNumber.normalize(this);
        return JSON.stringify(this.data);
    }

    /**
     * 转换为显示用的字符串,用于界面显示.
     * @param bigNumber 是否显示大数字:即10B 将显示为10000A
     */
    public simpleValue(bigNumber: boolean = false): string {
        BigNumber.normalize(this);
        let unit = this.getMaxUnit();

        let v = this.data[unit] || 0;
        if (unit > Unit.zero && this.data.hasOwnProperty(unit - 1)) {
            v += this.data[unit - 1] / UNIT_BASE;
        }

        if (bigNumber && v < 100 && unit > Unit.zero) {
            v = (v * UNIT_BASE);
            unit--;
        }

        let v2 = "";
        if (unit <= Unit.zero) {
            v2 = v.toFixed(0);
        } else {
            v2 = v.toFixed(2);
        }

        // 包含小数点最多4位.不包含小数点,则为3位.
        let pointIndex = v2.indexOf(".");
        if (pointIndex >= MAX_LEN) {
            v2 = v2.substring(0, pointIndex);
        } else if (pointIndex >= 0) {
            v2 = v2.substring(0, MAX_LEN + 1);
        }
        return `${v2}${unit == Unit.zero ? "" : Unit[unit]}`;
    }

    /**
     * 如果确认此数值可以安全转换为Number,则可以调用此接口.
     */
    public toNumber(): Number {
        BigNumber.normalize(this);
        let unit = this.getMaxUnit();
        let v = this.data[unit] || 0;
        if (unit > Unit.zero && this.data.hasOwnProperty(unit - 1)) {
            v += this.data[unit - 1] / UNIT_BASE;
        }
        return Math.round(v * Math.pow(UNIT_BASE, unit));
    }

    /**
     *  转为打印用字符串,1c2b3a...
     */
    public toString(): string {
        this.zipValue();
        let str = "", temp = 0;
        for (let u in this.data) {
            if (this.data.hasOwnProperty(u)) {
                temp = parseInt(u, 10);
                if (temp == Unit.zero) {
                    str = `${this.data[temp]}`;
                } else {
                    str = `${this.data[temp]}${Unit[temp]}${str}`;
                }
            }
        }
        if (str.length == 0) {
            str = "0";
        }
        return str;
    }

    /**
     * 加法一: 直接加上一个单位的大数值.会自动对数值进行归一化处理.
     * @param v 小数值.
     * @param unit 单位.
     * @param normalized 是否进行归一化处理.
     */
    public add(v: number, unit: Unit = Unit.zero, normalized: boolean = false): BigNumber {
        return this.addNum(new BigNumber(v, unit), normalized);
    }

    /**
     * 直接将当前数据设置为新的数值. 等效于赋值操作. 即将一个大数值设定为另一个值.
     * @param v
     * @param unit
     */
    public set(v: number, unit: Unit = Unit.zero) {
        this.data = {}
        this.data[unit] = v;
    }

    /**
     * 设置某一个单位的数值.不影响其他单位. 等效于对某一精度单位赋值.
     * @param v
     * @param unit
     */
    public setField(v: number, unit: Unit = Unit.zero) {
        // this.data[unit] = 0;
        // this.add(v, unit);
        this.data[unit] = v;
    }

    /**
     * 获取某一单位下的数值.
     * @param unit
     */
    public getField(unit: Unit = Unit.zero): number {
        return this.data[unit] || 0;
    }

    /**
     * 大数字加法/减法
     * @param bigNum
     * @param normalized 是否进行归一化压缩处理. 如果需要连加,可不进行压缩处理,以节省性能.
     */
    public addNum(bigNum: BigNumber, normalized: boolean = false): BigNumber {
        for (let v2 in bigNum.data) {
            if (this.data.hasOwnProperty(v2)) {
                this.data[v2] += bigNum.data[v2];
            } else {
                this.data[v2] = bigNum.data[v2];
            }
        }
        if (normalized) {
            BigNumber.normalize(this);
        }
        return this;
    }

    /**
     *  减法运算.转换为加法运算
     */
    public sub(v: number, unit: Unit = Unit.zero): BigNumber {
        return this.addNum(new BigNumber(-v, unit));
    }

    /**
     * 减法运算,对减数取反后转换为加法.
     */
    public subNum(bigNum: BigNumber, normalized: boolean = false): BigNumber {
        return this.addNum(bigNum.clone().negate(), normalized);
    }

    /**
     * 对源数据进行取反操作.取符号位变为相反值.
     */
    public negate(): BigNumber {
        for (let u in this.data) {
            if (this.data.hasOwnProperty(u)) {
                this.data[u] = -this.data[u];
            }
        }
        return this;
    }

    /**
     * 乘法运算. 实际是将两个乘数的各精度单位展开后分别相乘最后再合并.
     * 时间复杂度: m*n 次乘法, m*n-1次加法.
     * @param num
     * @param normalized
     */
    public multipleSelf(num: BigNumber, normalized: boolean = false): BigNumber {
        this.copy(this.multiple(num, normalized));
        return this;
    }

    /**
     * 生成新的对象. 不在原对象基础上叠加.
     * @param num
     * @param normalized
     */
    public multiple(num: BigNumber, normalized: boolean = false): BigNumber {
        // 计算的过程,不能影响原数值的数据
        let tempNumber = new BigNumber();
        // 遍历两个待乘数的所有单位,相乘后叠加.
        for (let u in this.data) {
            if (this.data.hasOwnProperty(u)) {
                tempNumber.addNum(num.clone().multiple2(this.data[u], parseInt(u, 10)));
            }
        }
        if (normalized) {
            BigNumber.normalize(tempNumber);
        }
        return tempNumber;
    }

    /**
     * 简易乘法: 乘数为单精度数值.即只有一个精度单位.
     * 时间复杂度: m次乘法, m次加法
     * @param value
     * @param unit
     * @param normalized
     */
    public multiple2(value: number, unit: Unit = Unit.zero, normalized: boolean = false): BigNumber {
        let tempNumber = new BigNumber();
        for (let u in this.data) {
            if (this.data.hasOwnProperty(u)) {
                tempNumber.add(this.data[u] * value, parseInt(u, 10) + unit);
            }
        }
        if (normalized) {
            BigNumber.normalize(tempNumber);
        }
        return tempNumber;
    }

    /**
     * 结果存储在自身.
     * @param value
     * @param unit
     * @param normalized
     */
    public multiple2Self(value: number, unit: Unit = Unit.zero, normalized: boolean = false): BigNumber {
        this.copy(this.multiple2(value, unit, normalized));
        return this;
    }

    /**
     * 除法.原理: 将多精度单位的除数,转换为单精度的除数. 此时可能会损失精度.
     * 默认取最大5位精度( 5*3= 15,即小数点后15位小数), 通常情况下除数最好不要有太多精度单位.避免损失数值
     * @param num
     * @param normalized
     */
    public div(num: BigNumber, normalized: boolean = false): BigNumber {
        let unit = num.getMaxUnit();
        let v = num.data[unit];
        let count = 5;
        for (let i = unit - 1; i >= Unit.zero; i--) {
            if (count-- <= 0) {
                break;
            }
            if (num.data.hasOwnProperty(i)) {
                v += num.data[i] / UNIT_BASE;
            }
        }
        return this.div2(v, unit, normalized);
    }

    /**
     * 除法,结果存储自身
     * @param num
     * @param normalized
     */
    public divSelf(num: BigNumber, normalized: boolean = false): BigNumber {
        let unit = num.getMaxUnit();
        let v = num.data[unit];
        let count = 5;
        for (let i = unit - 1; i >= Unit.zero; i--) {
            if (count-- <= 0) {
                break;
            }
            if (num.data.hasOwnProperty(i)) {
                v += num.data[i] / UNIT_BASE;
            }
        }
        return this.div2Self(v, unit, normalized);
    }

    /**
     * 简易除法运算, 最终会转换为乘法运算.
     * @param value
     * @param unit
     * @param normalized
     */
    public div2(value: number, unit: Unit = Unit.zero, normalized: boolean = false): BigNumber {
        if (value == 0) {
            console.error("big number div / 0");
            return this;
        }
        return this.multiple2(1 / value, -unit, normalized);
    }

    /**
     * 结果改变自身.
     * @param value
     * @param unit
     * @param normalized
     */
    public div2Self(value: number, unit: Unit = Unit.zero, normalized: boolean = false): BigNumber {
        if (value == 0) {
            console.error("big number div / 0");
            return this;
        }
        return this.multiple2Self(1 / value, -unit, normalized);
    }


    /**
     * 比较运算,greatThan(), > < ,没有== 需要注意.
     * @param other
     */
    public gt(other: BigNumber): boolean {
        let num = this.clone().subNum(other, true);
        let u = num.getMaxUnit();
        return num.getField(u) > 0;
    }

    /**
     *  是否==0
     */
    public equalTo0(): boolean {
        this.zipValue();
        if (Object.keys(this.data).length == 1 && this.data[Unit.zero] === 0) {
            return true
        }
        return false;
    }

    /**
     * 判断是否> 0
     */
    public gt0(): boolean {
        BigNumber.normalize(this);
        let unit = this.getMaxUnit();
        return this.data[unit] > 0;
    }

    /**
     *  克隆一个新的数字出来.因为加减运算等操作会影响原操作数.
     */
    public clone(): BigNumber {
        let num = new BigNumber();
        // @ts-ignore
        Object.assign(num.data, this.data);
        return num
    }

    /**
     *  复制 src 数据到当前数据中.
     * @param src
     */
    public copy(src: BigNumber) {
        this.data = {};
        // @ts-ignore.
        Object.assign(this.data, src.data);
    }

    /**
     *  压缩数据.移除其中为0的键
     */
    public zipValue() {
        // 删除其中为0的项.
        for (let u in this.data) {
            if (this.data[u] == 0) {
                delete this.data[u];
            }
        }
        // 如果没数据了,添加0
        if (Object.keys(this.data).length == 0) {
            this.data[Unit.zero] = 0;
        }
    }

    /**
     *  获取数值最大单位.会同时压缩数据
     */
    public getMaxUnit(): Unit {
        this.zipValue();
        const unites = this.unites();
        return unites.length <= 0 ? Unit.zero : unites[unites.length - 1];
    }

    /**
     *  按顺序获取包含的所有精度单位.
     */
    public unites(): Unit[] {
        return Object.keys(this.data).map((value1) => {
            return parseInt(value1);
        }).sort((a, b) => {
            return a >= b ? 1 : -1;
        })
    }

    private formatValue(_value: number, _unit: Unit = Unit.zero) {
        let value = _value;
        let unit = _unit;
        if (unit > UNIT_MAX) {
            console.error("单位超长.")
        } else if (unit < Unit.zero) {
            // 单位有可能为负, 比如小数/大数. a/d
            let m = Unit.zero - unit;
            value = value / Math.pow(UNIT_BASE, m);
            unit = Unit.zero;
        }
        let symb = value >= 0 ? 1 : -1;
        let v2 = value * symb;
        let u2 = unit;
        if (v2 >= UNIT_BASE) {
            let temp = 0;
            while (v2 > UNIT_BASE) {
                if (u2 >= UNIT_MAX) {
                    break;
                }
                temp = v2 % UNIT_BASE;
                if (temp != 0) {
                    this.data[u2] = symb * temp;
                }
                v2 = Math.floor(v2 / UNIT_BASE);
                u2++;
            }
            if (v2 != 0) {
                this.data[u2] = symb * v2;
            }
        } else if (v2 < 1) {
            let temp = 0;
            while (v2 < 1) {
                if (u2 <= Unit.zero) {
                    break;
                }
                v2 *= UNIT_BASE;
                u2--;
            }
            if (v2 != 0) {
                this.data[u2] = symb * parseFloat(v2.toFixed(2));
            }
        } else {
            this.data[unit] = value;
            return;
        }
    }
}